/**
 * creation: 8-mei-2008
 */
package org.jfonia.musicxml;

import java.util.List;

import org.jfonia.musicxml.model.Attributes;
import org.jfonia.musicxml.model.Key;
import org.jfonia.musicxml.model.Measure;
import org.jfonia.musicxml.model.MeasureElement;
import org.jfonia.musicxml.model.Time;
import org.jfonia.musicxml.model.TimeModification;
import org.jfonia.musicxml.model.Value;
import org.jfonia.pitch.MusicalInterval;
import org.jfonia.rhythm.CDuration;
import org.jfonia.rhythm.RhythmNotation;
import org.jfonia.staffnotation.BasicKeySignature;
import org.jfonia.staffnotation.BasicTimeSignature;
import org.jfonia.staffnotation.BasicClef;
import org.jfonia.util.JFoniaException;
import org.jfonia.util.Math;

/**
 * @author wijnand.schepens@gmail.com
 *
 */
public class Part
{
	protected org.jfonia.musicxml.model.Part xmlPart = null;

	protected int divisions = -1; // "not set"

        /*FIXME: UNCOMMENT
	protected IndexedFamily metricFamily = new IndexedFamily();
	protected IndexedFamily clefFamily = new IndexedFamily();
	protected IndexedFamily timeSignatureFamily = new IndexedFamily();
	protected IndexedFamily keySignatureFamily = new IndexedFamily();
	protected IndexedFamily noteFamily = new IndexedFamily();
	protected IndexedFamily tupletFamily = new IndexedFamily();
	protected IndexedFamily tieFamily = new IndexedFamily();
	*/
	
	public Part(org.jfonia.musicxml.model.Part xmlPart) throws JFoniaException
	{
            /*FIXME: uncomment
		this.xmlPart = xmlPart;

		ArrayList<Integer> measureTimeList = new ArrayList<Integer>();
		
		List<Integer> clefTimeList = new ArrayList<Integer>();
		List<BasicClef>    clefList = new ArrayList<BasicClef>();
	
		List<Integer>      keySigTimeList = new ArrayList<Integer>();
		List<BasicKeySignature> keySigList = new ArrayList<BasicKeySignature>();
	
		List<Integer>             timeSigTimeList = new ArrayList<Integer>();
		List<BasicTimeSignature>  timeSigList = new ArrayList<BasicTimeSignature>();
	
		// TODO: bars or measures ??
	
		// notes ("notated" notes, possibly part of a group of tied notes with one pitch)
		ArrayList<Integer>   noteTimeList = new ArrayList<Integer>();
		ArrayList<MusicalInterval>  notePitchList = new ArrayList<MusicalInterval>();
		ArrayList<CDuration> noteCDurationList = new ArrayList<CDuration>();
	
		ArrayList<Integer> tupletStartTimeList = new ArrayList<Integer>();
		ArrayList<Integer> tupletStopTimeList = new ArrayList<Integer>();
		ArrayList<TimeModification> tupletList = new ArrayList<TimeModification>();	// TODO: don't use musicxml model element
		
	//	List<Boolean>   noteTiedToNextList = new ArrayList<Boolean>();   // alternative: separate time list
	//	ArrayList<Integer>   tieNoteIndexList = new ArrayList<Integer>();	// indicates tie to next note
		ArrayList<Integer> tieTimeList = new ArrayList<Integer>();
		
	//	ArrayList<Integer>   slurStartTimeList = new ArrayList<Integer>();	// before first note 
	//	ArrayList<Integer>   slurStopTimeList = new ArrayList<Integer>();	// before last note
	
		
		
		List<Measure> measureList = xmlPart.getMeasureList();

		determineDivisions(measureList);
		
		
		int currTime = 0;
		for (Measure m: measureList)
		{
			measureTimeList.add(currTime);
			
			for (MeasureElement me: m.getElements())
			{
				if (me instanceof Note)
				{
					Note note = (Note)me;
					noteTimeList.add(currTime);
					
					MusicalInterval iv = convertPitch(note.getPitch());
					notePitchList.add(iv);
					
					CDuration du = calculateDuration(note.getDuration(), divisions, note.getTimeModification());
						// throws
					// TODO: check consistency with type (if present)
					noteCDurationList.add(du);
					
					if (note.getTuplet() != null)
					{
						if ("start".equals(note.getTuplet().getType()))
						{
							tupletStartTimeList.add(currTime);
							tupletList.add(note.getTimeModification());	// assume non null (TODO)
						}
						else // assume type is 'stop' (TODO)
						{
							tupletStopTimeList.add(currTime);
						}
					}
					
					if (note.isTieStart())
						//tieNoteIndexList.add(noteTimeList.size()-1);	// index of current (last) note
						tieTimeList.add(currTime);
					
					currTime += note.getDuration();
					
				}
				else if (me instanceof Attributes)
				{
					Attributes attr = (Attributes)me;
					
					org.jfonia.musicxml.model.Clef xmlClef = attr.getClef();
					if (xmlClef != null)
					{
						clefTimeList.add(currTime);
						clefList.add(convertClef(xmlClef));
					}
					
					Time xmlTimeSig = attr.getTime();
					if (xmlTimeSig != null)
					{
						timeSigTimeList.add(currTime);
						timeSigList.add(convertTimeSig(xmlTimeSig));
					}
					
					Key xmlKeySig = attr.getKey();
					if (xmlKeySig != null)
					{
						keySigTimeList.add(currTime);
						keySigList.add(convertKeySig(xmlKeySig));
					}
					
				}
				// ...
			}
		}
		//measureTimeList.add(currTime);	// after last (could be partial??)
		
		this.metricFamily.putList("time", measureTimeList);
		
		this.clefFamily.putList("time", clefTimeList);
		this.clefFamily.putList("clef", clefList);
		
		this.noteFamily.putList("time", noteTimeList);
		this.noteFamily.putList("pitch", notePitchList);	
		this.noteFamily.putList("duration", noteCDurationList);
	
		this.tupletFamily.putList("startTime", tupletStartTimeList);
		this.tupletFamily.putList("stopTime", tupletStopTimeList);
		this.tupletFamily.putList("tuplet", tupletList);	
			// TODO: don't use musicxml model element
		
		this.tieFamily.putList("time", tieTimeList);	
		
	//	ArrayList<Integer>   slurStartTimeList = new ArrayList<Integer>();	// before first note 
	//	ArrayList<Integer>   slurStopTimeList = new ArrayList<Integer>();	// before last note

		this.timeSignatureFamily.putList("time", timeSigTimeList);
		this.timeSignatureFamily.putList("timeSig", timeSigList);

		this.keySignatureFamily.putList("time", keySigTimeList);
		this.keySignatureFamily.putList("keySig", keySigList);
           */
	}
	
	
	private BasicKeySignature convertKeySig(Key xmlKeySig)
	{
		return new BasicKeySignature(xmlKeySig.getFifths());
	}


	private BasicTimeSignature convertTimeSig(Time xmlTimeSig)
	{
		return new BasicTimeSignature(xmlTimeSig.getBeats(), xmlTimeSig.getBeatType());
	}


	private BasicClef convertClef(org.jfonia.musicxml.model.Clef xmlClef)
	{
		String sign = xmlClef.getSign();
		if (sign.equals("G"))
			return BasicClef.trebleClef;
		else if (sign.equals("F"))
			return BasicClef.bassClef;
		else
			throw new RuntimeException("Unknown clef '" + sign + "'");
			// TODO
	}


	protected void determineDivisions(List<Measure> measureList)
	{
		if (measureList.size()==0)
			throw new RuntimeException("empty part??");
		Measure m0 = measureList.get(0);
		if (m0.getMeasureElementList().size()==0)
			throw new RuntimeException("first measure empty??");
		MeasureElement me = m0.getMeasureElementList().get(0);
		if (!(me instanceof Attributes))
			throw new RuntimeException("Expected first measure element to be Attributes");
		Attributes attr = (Attributes)me;
		int div = attr.getDivisions();
		if (div == 0)
			throw new RuntimeException("Expected first Attributes element to set divisions");
		this.divisions = div;
	}
	
	protected MusicalInterval convertPitch(org.jfonia.musicxml.model.Pitch xmlPitch)
	{
		if (xmlPitch == null)
			return null;		// indicates a rest
		
		String stepStr = xmlPitch.getPitchClass().getStep();
		int step = "CDEFGAB".indexOf(stepStr);
		if (step==-1)
			throw new RuntimeException("step should be A,B,C,D,E,F or G");	
				// TODO: more possibilities
				// TODO: throw different kind of exception
		
		int alt = xmlPitch.getPitchClass().getAlter();	// 1 for sharp, -1 for flat...
		
		int oct = xmlPitch.getOctave() - 4;	// according to musicxml middle C is in octave 4; in jfonia it in octave 0
		
		return new MusicalInterval(oct*7 + step, alt);
	}
	
//	protected CDuration convertValue(Value value)
//	{
//		int recip = value.getRecip();
//		return new CDuration(recip-1, value.getNumDots());
//	}
	
	protected CDuration convertValue(Value value) throws JFoniaException
	{
		final String[] types = { "long", "breve", "whole", "half", "quarter", "eighth", "16th", "32nd", "64th", "128th", "256th" };
		// TODO: use map
		// TODO: user specific?
		
		String type = value.getType();
		
		int ival=0;
		while (ival<types.length && !type.equals(types[ival]))
			ival++;
		if (ival >= types.length)
			throw new RuntimeException("Unsupported note type '" + type + "'");
		
		int numDots = value.getNumDots();
		
		return new CDuration(ival-2, numDots); // throws
	}

	/** 
	 * calculate notated value (note-length) given a duration (in ticks), taking into account tuplets.
	 */
	protected CDuration calculateDuration(int dur, int divisions, TimeModification timeModification) throws JFoniaException
	{
		if (timeModification == null)
			return calculateDuration(dur, divisions, 1, 1);
		else
			return calculateDuration(dur, divisions, timeModification.getActualNotes(), timeModification.getNormalNotes());
	}


	protected CDuration calculateDuration(int dur, int divisions, int actualNotes, int normalNotes) throws JFoniaException
	{
		int nom = dur * actualNotes;
		int denom = 4 * divisions * normalNotes;
//		//System.out.println(""+ dur +" " + actualNotes + " " + normalNotes);
//		// e.g. : divisions=48, dur=16, act=3, nor=2 (part of triplet)
//		// then nom/denom = 1/8
//
//		// normalize fraction:
		int gcd = Math.calculateGCD(nom, denom);
		nom /= gcd;
		denom /= gcd;

		int numDots = 0;
		if (nom % 3 == 0)
		{
			numDots = 1;
			nom /= 3;
			if (denom % 2 == 0)
				denom /= 2;
			else
				nom *= 2;
		}
		if (nom % 7 == 0)
		{
			numDots = 2;
			nom /= 7;
			if (denom % 4 == 0)
				denom /= 4;
			else if (denom % 2 == 0)
			{
				denom /= 2;
				nom *= 2;
			}
			else
				nom *= 4;
		}
		
		if (nom==2 && denom==1)
			return new CDuration(CDuration.BREVE, numDots);
		else if (nom==3 && denom==1)
			return new CDuration(CDuration.LONG, numDots);
		else if (nom==1)
		{
			int log = RhythmNotation.log2(denom);	// throws if not a denom is not a power of 2 !
			return new CDuration(log, numDots);
		}
		else
			//throw new CodedMusicXMLImportException("VALUE_TOO_LONG", "", refNode);
			throw new JFoniaException("Duration cannot be represented as a CDuration");

		
	}
	
//	protected Value calculate(int dur, int divisions, TimeModification timeModification, Node refNode) throws CodedMusicXMLImportException
//	{
//		if (timeModification == null)
//			return calculate(dur, divisions, 1, 1, refNode);
//		else
//			return calculate(dur, divisions, timeModification.getActualNotes(), timeModification.getNormalNotes(), refNode);
//	}
//
//	/**
//	 * Calculate value from duration. 
//	 */
//	protected Value calculate(int dur, int divisions, int actualNotes, int normalNotes, Node refNode) throws CodedMusicXMLImportException
//	{
//
//		// calculate notated value (note-length), taking into account tuplets...
//		int nom = dur * actualNotes;
//		int denom = 4 * divisions * normalNotes;
//		//System.out.println(""+ dur +" " + actualNotes + " " + normalNotes);
//		// e.g. : divisions=48, dur=16, act=3, nor=2 (part of triplet)
//		// then nom/denom = 1/8
//
//		// normalize fraction:
//		int gcd = Math.calculateGCD(nom, denom);
//		nom /= gcd;
//		denom /= gcd;
//
//		int numDots = 0;
//		if (nom % 3 == 0)
//		{
//			numDots = 1;
//			nom /= 3;
//			if (denom % 2 == 0)
//				denom /= 2;
//			else
//				nom *= 2;
//		}
//		if (nom % 7 == 0)
//		{
//			numDots = 2;
//			nom /= 7;
//			if (denom % 4 == 0)
//				denom /= 4;
//			else if (denom % 2 == 0)
//			{
//				denom /= 2;
//				nom *= 2;
//			}
//			else
//				nom *= 4;
//		}
//		
//		
//		if (nom != 1)
////			throw logger.log(new CodedMusicXMLImportException("VALUE_TOO_LONG", "", refNode));
//			throw new CodedMusicXMLImportException("VALUE_TOO_LONG", "", refNode);
//
//		// check whether denom is power of 2:
//		int p = 1;
//		while (p < denom)
//		{
//			p *= 2;
//		}
//		if (p > denom)
////			throw internalLogger.log(new CodedMusicXMLImportException("TUPLET_PROBLEM", "" + p + "," + denom, refNode));
//			throw new CodedMusicXMLImportException("TUPLET_PROBLEM", "" + p + "," + denom, refNode);
//
////		return new Value(refNode, null, denom, numDots);
//		return new Value(refNode, denom, numDots);
//	}

	
//	/**
//	 * check whether two values are equivalent.
//	 */
//	protected void checkConsistency(Value value1, Value value2, Node node) throws CodedMusicXMLImportException
//	{
//		if (value1.getRecip() != 0 && value1.getRecip() != value2.getRecip())
////			throw logger.log(new CodedMusicXMLImportException("VALUE_INCONSISTENT_TYPE", "" + value1.getRecip() + "," + value2.getRecip(), node));
//			throw new CodedMusicXMLImportException("VALUE_INCONSISTENT_TYPE", "" + value1.getRecip() + "," + value2.getRecip(), node);
//		if (value1.getNumDots() > 0 && value1.getNumDots() != value2.getNumDots())
////			throw logger.log(new CodedMusicXMLImportException("VALUE_INCONSISTENT_DOTS", "" + value1.getNumDots() + "," + value2.getNumDots(), node));
//			throw new CodedMusicXMLImportException("VALUE_INCONSISTENT_DOTS", "" + value1.getNumDots() + "," + value2.getNumDots(), node);
//	}




	public org.jfonia.musicxml.model.Part getXmlPart()
	{
		return xmlPart;
	}

	public int getDivisions()
	{
		return divisions;
	}

        /*FIXME:UNCOMMENT
	public Family getClefFamily()
	{
		return clefFamily;
	}
        

	public Family getKeySignatureFamily()
	{
		return keySignatureFamily;
	}


	public Family getMetricFamily()
	{
		return metricFamily;
	}


	public Family getNoteFamily()
	{
		return noteFamily;
	}


	public Family getTieFamily()
	{
		return tieFamily;
	}


	public Family getTimeSignatureFamily()
	{
		return timeSignatureFamily;
	}


	public Family getTupletFamily()
	{
		return tupletFamily;
	}
        */

}
